package mysql

import (
	"database/sql"
	"fmt"
	"runtime"
	"strings"
	"sync"
	"time"

	mstrs "gitee.com/yrwy/msgo/pkg/strs"
	msync "gitee.com/yrwy/msgo/pkg/sync"
	"github.com/go-sql-driver/mysql"
)

var (
	handler map[string]*sql.DB
	_rwm    sync.RWMutex
)

func init() {
	handler = make(map[string]*sql.DB)
}

func RegistMysql(name string, opts ...Option) error {
	db, err := NewDriver(opts...)
	if err != nil {
		return err
	}
	msync.LockGuard(&_rwm, func() {
		handler[name] = db
	})
	return nil
}

func GetDB(name string) *sql.DB {
	_rwm.RLock()
	defer _rwm.RUnlock()
	db, _ := handler[name]
	return db
}

type options struct {
	c       *mysql.Config
	maxIdle int
	maxOpen int
}

func DefaultOptions() *options {
	c := mysql.NewConfig()
	c.Addr = "127.0.0.1:3306"
	c.DBName = "mysql"
	c.User = "root"
	c.Passwd = "123456"
	c.Timeout = time.Second * 2
	return &options{
		c:       c,
		maxIdle: 1,
		maxOpen: runtime.NumCPU() + 1,
	}
}

var defaultOptions = options{
	c:       mysql.NewConfig(),
	maxIdle: 1,
	maxOpen: runtime.NumCPU() + 1,
}

// interface option的优缺点：不可以在包外扩展options，除非apply函数开头大写
type Option interface {
	apply(*options)
}

type EmptyOption struct{}

func (EmptyOption) apply(*options) {}

// 如果要在包外扩展options，funcOption开头必须大小
type funcOption func(*options)

func (fdo funcOption) apply(do *options) {
	fdo(do)
}

func Address(addr string) Option {
	return funcOption(func(o *options) {
		o.c.Net = "tcp"
		o.c.Addr = addr
		ip, _, _ := strings.Cut(addr, ":")
		switch ip {
		case "10.143.6.17":
			o.c.User = "root"
			o.c.Passwd = "Hngs@#9102"
		case "10.199.199.44":
			o.c.User = "root"
			o.c.Passwd = "Xxkj@2023"
		}
	})
}

func Database(db string) Option {
	return funcOption(func(o *options) {
		o.c.DBName = db
	})
}

func User(user string) Option {
	return funcOption(func(o *options) {
		o.c.User = user
	})
}

func Password(psw string) Option {
	return funcOption(func(o *options) {
		o.c.Passwd = psw
	})
}

func Timeout(t time.Duration) Option {
	return funcOption(func(o *options) {
		o.c.Timeout = t
	})
}

func ReadTimeout(t time.Duration) Option {
	return funcOption(func(o *options) {
		o.c.ReadTimeout = t
	})
}

func WriteTimeout(t time.Duration) Option {
	return funcOption(func(o *options) {
		o.c.WriteTimeout = t
	})
}

func Charset(charset string) Option {
	return funcOption(func(o *options) {
		if o.c.Params == nil {
			o.c.Params = make(map[string]string)
		}
		o.c.Params["charset"] = charset
	})
}

func MaxIdleConns(n int) Option {
	return funcOption(func(o *options) {
		o.maxIdle = n
	})
}

func MaxOpenConns(n int) Option {
	return funcOption(func(o *options) {
		o.maxOpen = n
	})
}

func (o *options) url() string {
	return o.c.FormatDSN()
}

func NewUrl(opts ...Option) string {
	sqlOpts := defaultOptions
	for _, o := range opts {
		o.apply(&sqlOpts)
	}
	return sqlOpts.url()
}

func NewDriver(opts ...Option) (*sql.DB, error) {
	sqlOpts := defaultOptions
	for _, o := range opts {
		o.apply(&sqlOpts)
	}
	url := sqlOpts.url()
	sqlDb, err := sql.Open("mysql", url)
	if err != nil {
		return nil, err
	}
	// if int64(sqlOpts.c.Timeout) > 0 {
	// 	ctx, cancel := context.WithTimeout(context.TODO(), sqlOpts.c.Timeout)
	// 	defer cancel()
	// 	err = sqlDb.PingContext(ctx)
	// } else {
	// 	err = sqlDb.Ping()
	// }
	err = sqlDb.Ping()
	if err != nil {
		return nil, err
	}
	sqlDb.SetMaxIdleConns(sqlOpts.maxIdle)
	sqlDb.SetMaxOpenConns(sqlOpts.maxOpen)
	return sqlDb, nil
}

func TransError(err error) (int, string) {
	me, ok := err.(*mysql.MySQLError)
	if !ok {
		return 0, ""
	}
	return int(me.Number), me.Message
}

// q not set "select count()"
func Count(db *sql.DB, q string, args ...any) int {
	if !mstrs.WithPrefixFold(strings.TrimSpace(q), "from ") {
		return -1
	}
	n := 0
	err := db.QueryRow("SELECT COUNT(1) "+q, args...).Scan(&n)
	if err != nil {
		return -1
	}
	return n
}

func Exists(db *sql.DB, q string, args ...any) bool {
	q = strings.TrimSpace(q)
	if !mstrs.WithPrefixFold(q, "SELECT EXISTS") {
		for strings.HasSuffix(q, ";") {
			q = q[:len(q)-1]
		}
		q = "SELECT EXISTS(" + q + ");"
	}
	n := 0
	err := db.QueryRow(q, args...).Scan(&n)
	if err != nil {
		return false
	}
	return n == 1
}

func HasTable(db *sql.DB, tname string) bool {
	var r string
	err := db.QueryRow(fmt.Sprintf("SHOW TABLES LIKE '%s';", tname)).Scan(&r)
	if err != nil {
		return false
	}
	if r == tname {
		return true
	}
	return false
}

func GetMaxAllowedPacket(db *sql.DB) int {
	var r int
	row, err := db.Query("SHOW VARIABLES LIKE 'max_allowed_packet';")
	if err != nil {
		return -1
	}
	defer row.Close()
	if row.Next() {
		var name sql.NullString
		err = row.Scan(&name, &r)
		if err != nil {
			return -1
		}
	}
	return r
}
